活动网(AOV、AOE)——数据结构系列教程(c++版)

梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)


AOV 网(Activity On Vertex,顶点表示活动的网)和 AOE 网(Activity On Edge,边表示活动的网)是带权有向无环图(DAG)的两大核心应用:

两者均要求图为有向无环图(DAG),若存在环则无拓扑排序(活动间循环依赖),也无关键路径(项目无法完成)。


图结构的实现方式:

  1. 邻接矩阵图:是一种使用「一维数组 + 二维数组」存储顶点和边数据的图结构,稠密图中常被采用;
  2. 邻接链表图:是一种使用「数组 + 链表」存储顶点和边数据的图结构,稀疏图中常被采用;

学习经典应用场景前,请根据上面的教程封装好自定义图,所有场景实例直接复用

教程目录导航

一、AOV 网与拓扑排序

1.1 AOV 网基本概念

AOV 网解决“先做什么,后做什么”(顺序问题)

1.2 拓扑排序定义

对 AOV 网的顶点进行线性排序,满足:若图中存在有向边,则排序中u一定在v之前。

1.3 拓扑排序经典算法:Kahn 算法(入度表 + 队列)

最易理解、易实现的拓扑排序算法,核心是反复删除入度为 0 的顶点,并更新其邻接顶点的入度,步骤如下:


#include <iostream>
#include <vector>
#include <queue>
#include"LinkedGraph.h"

using namespace std;

// 打印拓扑序列(支持顶点名称映射)
void printKahnSort(const LinkedGraph& adj, vector<int>& topoOrder) {
    int n = topoOrder.size();
    if (n != adj.vertexNum) {
        cout << "无有效拓扑序列!" << endl;
        return;
    }
    cout << "Kahn算法求拓扑序列:";
    for(int i=0;i < n;i++)
    {
        cout << adj.adjList[topoOrder[i]].data;
        if(i != n-1)
        {
            cout << "->";
        }
    }
    cout << endl;
}

// 拓扑排序:Kahn算法
// adj-邻接表,topoOrder-输出拓扑序列
// 返回值:true-排序成功(无环),false-排序失败(有环)
bool kahnSort(const LinkedGraph& adj, vector<int>& topoOrder) {
    int n = adj.vertexNum;// 顶点数
    vector<int> inDegree(n, 0);// 入度数组
    queue<int> q;

    // 计算所有顶点的入度,存入入度数组inDegree[];
    for (int u = 0; u < n; ++u) {
        EdgeNode* edge = adj.adjList[u].firstedge;
        while(edge != nullptr)
        {
            inDegree[edge->adjvex]++;
            edge = edge->next;
        }
        delete edge;
    }

    // 初始化队列,将所有入度为 0的顶点入队(初始可执行的活动);
    for (int i = 0; i < adj.vertexNum; ++i) {
        if (inDegree[i] == 0) {
            q.push(i);
        }
    }

    // 迭代处理:
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        topoOrder.push_back(u);  // 出队顶点u,将其加入topoOrder[];

        // 遍历u的所有邻接顶点,更新入度
        EdgeNode* edge = adj.adjList[u].firstedge;
        while(edge != nullptr)
        {
            int v = edge->adjvex;
            inDegree[v]--;// 将v的入度减 1(u完成,v的一个前驱依赖消除);
            if (inDegree[v] == 0) {
                q.push(v);// 若v的入度变为 0,将v入队(v的所有前驱完成,可执行)。
            }
            edge = edge->next;
        }
        delete edge;
    }

    // 拓扑序列长度等于顶点数,说明无环,排序成功
    return topoOrder.size() == n;
}

// 测试案例
int main() {
    LinkedGraph graph;
    vector<int> topoOrder;// 拓扑序列

    // 1. 添加顶点(A、B、C、D、E)
    graph.addVertex('A');
    graph.addVertex('B');
    graph.addVertex('C');
    graph.addVertex('D');
    graph.addVertex('E');

    // 2. 添加边(无权有向图)
    graph.addDirectedEdge('A','B',1);
    graph.addDirectedEdge('A','C',1);
    graph.addDirectedEdge('A','D',1);

    graph.addDirectedEdge('B','E',1);
    graph.addDirectedEdge('C','E',1);
    graph.addDirectedEdge('D','E',1);    

    // 3. 打印邻接矩阵
    graph.printAdjacency();

    // 4. 求拓扑序列
    kahnSort(graph, topoOrder);

    // 5. 打印拓扑序列
    printKahnSort(graph, topoOrder);

    return 0;
}
        

输出结果


===== 邻接表(带权)=====
A -> (D, 1) (C, 1) (B, 1)
B -> (E, 1)
C -> (E, 1)
D -> (E, 1)
E ->
Kahn算法求拓扑序列:A->D->C->B->E
            

二、AOE 网与关键路径

1.1 AOV 网基本概念

AOE 网解决“最快多久完成,哪些活动不能延迟”(时间优化问题);

AOE 网是带权有向无环图(DAG),是 AOV 网的扩展(关注活动时间和项目工期),核心概念:

1.2 关键路径求解核心:4 个关键数组

设顶点数为n,顶点编号0∼n−1,源点为0,汇点为n−1,定义 4 个核心数组(基础版基于邻接表实现):

1.3 关键路径求解步骤(基于拓扑排序)

关键路径依赖拓扑排序(保证正向推导ve[]时,前驱顶点已处理)和逆拓扑排序(保证反向推导vl[]时,后继顶点已处理),核心分 3 步:


#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include"LinkedGraph.h"

using namespace std;

bool isBuilt;              // 标记图是否已构建
int source;                // 源点(入度0)
int sink;                  // 汇点(出度0)
int vertexNum;             // 顶点数(事件数)

// 检查AOE网合法性:必须是DAG、仅有一个源点、仅有一个汇点
bool checkValid(const LinkedGraph& adj, const vector<int> inDegree, const vector<int>& topoOrder) {
    if (topoOrder.empty()) {
        cerr << "错误:AOE网存在环,不合法!" << endl;
        return false;
    }
    // 找源点(入度0)
    vector<int> sourceList;
    for (int i = 0; i < vertexNum; i++) {
        if (inDegree[i] == 0) sourceList.push_back(i);
    }
    // 找汇点(出度0)
    vector<int> sinkList;
    for (int i = 0; i < vertexNum; i++) {
        if (adj.adjList[i].firstedge == nullptr) sinkList.push_back(i);
    }
    if (sourceList.size() != 1) {
        cerr << "错误:AOE网需仅有一个源点,当前找到" << sourceList.size() << "个!" << endl;
        return false;
    }
    if (sinkList.size() != 1) {
        cerr << "错误:AOE网需仅有一个汇点,当前找到" << sinkList.size() << "个!" << endl;
        return false;
    }
    source = sourceList[0];
    sink = sinkList[0];
    return true;
}

// 查找从start到end的所有路径
void dfsFindPath(const vector<vector<int>>& adj, int start, int end,
                 vector<int>& path, vector<vector<int>>& res) {
    path.push_back(start);
    if (start == end) {
        res.push_back(path);
        path.pop_back();
        return;
    }
    for (int v : adj[start]) {
        dfsFindPath(adj, v, end, path, res);
    }
    path.pop_back();
}

// 打印关键路径(由关键活动组成,从源点到汇点)
void printCriticalPath(const vector<pair<int, int>>& criticalAct) {
    if (criticalAct.empty() || source == -1 || sink == -1) {
        cout << "无有效关键路径!" << endl;
        return;
    }
    // 构建关键活动的邻接表
    vector<vector<int>> cpAdj(vertexNum);
    for (const auto& p : criticalAct) {
        cpAdj[p.first].push_back(p.second);
    }
    // 深度优先搜索(DFS)查找从源点到汇点的关键路径
    vector<int> path;
    vector<vector<int>> allCriticalPaths;
    dfsFindPath(cpAdj, source, sink, path, allCriticalPaths);

    // 输出所有关键路径
    cout << "AOE网所有关键路径(从源点" << source << "到汇点" << sink << "):" << endl;
    for (size_t i = 0; i < allCriticalPaths.size(); i++) {
        cout << "  路径" << i+1 << ":";
        for (int v : allCriticalPaths[i]) {
            cout << adj.adjList[v].data;
            if (v != sink) cout << " -> ";
        }
        cout << endl;
    }
}

// 求关键路径:返回关键活动(pair<u,v>),输出工程最短时间、ve、vl、关键活动
vector<pair<int, int>> criticalPath(const LinkedGraph& adj, int& minProjectTime,vector<int>& topoOrder) {
    vector<pair<int, int>> criticalActivities; // 存储关键活动<u,v>
    vector<int> inDegree(vertexNum, 0);// 入度数组
    minProjectTime = 0;
    if (!isBuilt || vertexNum == 0) {
        cerr << "错误:AOE网未构建或无顶点!" << endl;
        return criticalActivities;
    }

    // 计算所有顶点的入度,存入入度数组inDegree[];
    for (int u = 0; u < vertexNum; ++u) {
        EdgeNode* edge = adj.adjList[u].firstedge;
        while(edge != nullptr)
        {
            inDegree[edge->adjvex]++;
            edge = edge->next;
        }
        delete edge;
    }

    // 步骤1:检查AOE网合法性
    if (!checkValid(adj, inDegree, topoOrder)) {
        return criticalActivities;
    }

    // 步骤2:求事件最早发生时间ve[]
    vector<int> ve(vertexNum, INT_MIN);
    ve[source] = 0; // 源点最早发生时间为0
    for (int u : topoOrder) { // 按拓扑顺序遍历
        EdgeNode* edge = adj.adjList[u].firstedge;
        while(edge != nullptr)
        {
            int v = edge->adjvex;
            int w = edge->weight;
            if (ve[v] < ve[u] + w) {
                ve[v] = ve[u] + w;
            }
            edge = edge->next;
        }
        delete edge;
    }
    minProjectTime = ve[sink]; // 汇点的ve就是工程最短完成时间

    // 步骤3:求事件最迟发生时间vl[]
    vector<int> vl(vertexNum, INT_MAX);
    vl[sink] = ve[sink]; // 汇点最迟发生时间=最早发生时间
    // 按逆拓扑顺序遍历(反转拓扑序列)
    reverse(topoOrder.begin(), topoOrder.end());
    for (int u : topoOrder) {
        EdgeNode* edge = adj.adjList[u].firstedge;
        while(edge != nullptr)
        {
            int v = edge->adjvex;
            int w = edge->weight;
            if (vl[u] > vl[v] - w) {
                vl[u] = vl[v] - w;
            }
            edge = edge->next;
        }
        delete edge;
    }

    // 步骤4:遍历所有边,计算松驰时间,找关键活动(slack=0)
    cout << "\n===== AOE网关键路径分析 =====" << endl;
    cout << "事件最早发生时间ve[]:";
    for (int x : ve) cout << x << " ";
    cout << "\n事件最迟发生时间vl[]:";
    for (int x : vl) cout << x << " ";
    cout << "\n工程最短完成时间:" << minProjectTime << endl;
    cout << "所有活动分析(u->v, 时间w, 最早开始时间, 最迟开始时间, 松驰时间):" << endl;

    for (int u = 0; u < vertexNum; u++) {
        EdgeNode* edge = adj.adjList[u].firstedge;
        while(edge != nullptr)
        {
            int v = edge->adjvex;
            int w = edge->weight;
            int e_activity = ve[u];          // 活动最早开始时间
            int l_activity = vl[v] - w;      // 活动最迟开始时间
            int slack = l_activity - e_activity; // 松驰时间
            cout << "  " << adj.adjList[u].data << "->" << adj.adjList[v].data << ", " << w << ", "
                 << e_activity << ", " << l_activity << ", " << slack << endl;
            // 松驰时间为0,是关键活动
            if (slack == 0) {
                criticalActivities.emplace_back(u, v);
            }
            edge = edge->next;
        }
        delete edge;
    }

    // 输出关键活动
    if (!criticalActivities.empty()) {
        cout << "关键活动(slack=0):";
        for (size_t i = 0; i < criticalActivities.size(); i++) {
            int u = criticalActivities[i].first;
            int v = criticalActivities[i].second;
            cout << adj.adjList[u].data << "->" << adj.adjList[v].data;
            if (i != criticalActivities.size() - 1) cout << ", ";
        }
        cout << endl;
    } else {
        cerr << "错误:未找到关键活动!" << endl;
    }

    return criticalActivities;
}


// 测试案例
int main() {
    LinkedGraph graph;
    vector<int> topoOrder;//拓扑序列
    int minProjectTime=0;

    // 1. 添加顶点(A、B、C、D、E、F、G、H)
    graph.addVertex('A');
    graph.addVertex('B');
    graph.addVertex('C');
    graph.addVertex('D');
    graph.addVertex('E');
    graph.addVertex('F');
    graph.addVertex('G');
    graph.addVertex('H');

    vertexNum = graph.vertexNum;
    vector<int> inDegree(vertexNum, 0);// 入度数组

    // 2. 添加边(带权有向图)
    graph.addDirectedEdge('A','B',7);
    graph.addDirectedEdge('A','C',6);

    graph.addDirectedEdge('B','E',4);

    graph.addDirectedEdge('C','D',3);
    graph.addDirectedEdge('C','E',5);

    graph.addDirectedEdge('D','F',2);
    graph.addDirectedEdge('D','H',5);

    graph.addDirectedEdge('E','F',3);
    graph.addDirectedEdge('E','G',4);

    graph.addDirectedEdge('F','H',2);

    graph.addDirectedEdge('G','H',4);

    isBuilt = true;

    // 3. 打印邻接矩阵
    graph.printAdjacency();

    // 4. 求拓扑序列
    kahnSort(graph, inDegree, topoOrder);

    // 5. 打印拓扑序列
    printKahnSort(graph, topoOrder);

    // 6. 求关键路径
    vector<pair<int, int>> criticalAct = criticalPath(graph, minProjectTime, topoOrder);

    // 7. 打印关键路径
    printCriticalPath(graph, criticalAct);

    return 0;
}
        

输出结果


===== 邻接表(带权)=====
A -> (C, 6) (B, 7)
B -> (E, 4)
C -> (E, 5) (D, 3)
D -> (H, 5) (F, 2)
E -> (G, 4) (F, 3)
F -> (H, 2)
G -> (H, 4)
H ->
Kahn算法求拓扑序列:A->C->B->D->E->G->F->H

===== AOE网关键路径分析 =====
事件最早发生时间ve[]:0 7 6 9 11 14 15 19
事件最迟发生时间vl[]:0 7 6 14 11 17 15 19
工程最短完成时间:19
所有活动分析(u->v, 时间w, 最早开始时间, 最迟开始时间, 松驰时间):
  A->C, 6, 0, 0, 0
  A->B, 7, 0, 0, 0
  B->E, 4, 7, 7, 0
  C->E, 5, 6, 6, 0
  C->D, 3, 6, 11, 5
  D->H, 5, 9, 14, 5
  D->F, 2, 9, 15, 6
  E->G, 4, 11, 11, 0
  E->F, 3, 11, 14, 3
  F->H, 2, 14, 17, 3
  G->H, 4, 15, 15, 0
关键活动(slack=0):A->C, A->B, B->E, C->E, E->G, G->H
AOE网所有关键路径(从源点0到汇点7):
  路径1:A -> C -> E -> G -> H
  路径2:A -> B -> E -> G -> H
            

三、典型应用场景

AOV 网的典型应用

  1. 课程选修系统:顶点表示课程,边表示先修关系,拓扑排序确定合理的选修顺序;
  2. 编译系统依赖解析:顶点表示源文件,边表示头文件依赖,拓扑排序确定文件的编译顺序;
  3. 任务调度系统:顶点表示任务,边表示任务间的依赖关系,拓扑排序确定任务的执行顺序。

AOE 网的典型应用

  1. 项目管理(PERT/CPM):如建筑工程、软件开发项目,顶点表示项目阶段(事件),边表示具体工作(活动),关键路径用于项目工期规划和资源优化;
  2. 生产流水线调度:顶点表示工序节点(事件),边表示加工工序(活动),关键路径用于确定流水线的瓶颈工序,优化生产效率;
  3. 物流配送规划:顶点表示物流节点(事件),边表示运输环节(活动),关键路径用于确定最快配送时间,优化配送流程。

返回顶部